package br.com.hs.nfe.certificado;

import br.com.hs.nfe.certificado.exception.HSCertificadoException;
import br.com.hs.nfe.certificado.exception.HSCertificadoInvalidoException;
import br.com.hs.nfe.certificado.exception.HSCertificadoSenhaException;
import br.com.hs.nfe.certificado.exception.HSCNPJInvalidoException;
import br.com.hs.nfe.common.to.CertificadoTO;
import br.com.hs.nfe.common.to.EmitenteTO;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * Classe responsável pelas regras de negócio do certificado.
 * @author Ranlive Hrysyk
 */
public class Certificado {

    /**
     * Tipo Certificado A1
     */
    private static final String A1 = "A1";
    private static final String KEYSTORE_INSTANCE = "PKCS12";

    /**
     * Obtém um certificado de um arquivo.
     */
    public static CertificadoTO createCertificadoTO(String caminhoArquivo, String Senha) throws HSCertificadoException {
        CertificadoTO certificadoTO = null;

        Map<X509Certificate, PrivateKey> map = recuperarCertificadosArquivo(caminhoArquivo, Senha);
        if (map != null) {
            Set set = map.entrySet();
            X509Certificate x509Certificate = (X509Certificate) ((Map.Entry) set.iterator().next()).getKey();
            certificadoTO = new CertificadoTO();
            certificadoTO.setX509Certificate(x509Certificate);
            certificadoTO.setPrivateKey(map.get(x509Certificate));
            certificadoTO.setDataValidadeFim(CertificadoHelper.getDataValidadeFim(x509Certificate));
            certificadoTO.setDataValidadeInicio(CertificadoHelper.getDataValidadeInicio(x509Certificate));
            certificadoTO.setCNPJ(CertificadoHelper.recuperarCNPJ(x509Certificate));
            certificadoTO.setAutoridadeCertificadora(CertificadoHelper.getEmissor(x509Certificate));
            certificadoTO.setAssunto(CertificadoHelper.getAssunto(x509Certificate));
            certificadoTO.setTipoCertificado(x509Certificate.getType());
            certificadoTO.setCaminho(caminhoArquivo);
            String fileName = new File(caminhoArquivo).getName();
            certificadoTO.setTipoCertificado(Certificado.A1);
            certificadoTO.setAlias("(" + certificadoTO.getTipoCertificado() + ") " + fileName + " " + certificadoTO.getCNPJ());
        }

        return certificadoTO;
    }

    /**
     * Recupera um certificado de um arquivo.
     */
    private static Map<X509Certificate, PrivateKey> recuperarCertificadosArquivo(String caminhoArquivo, String senha) throws HSCertificadoException {
        Map certificadosValidos = new HashMap();
        File certFile = new File(caminhoArquivo);
        if (!certFile.exists()) {
            throw new HSCertificadoException(MessageFormat.format(CertificadoConstants.ERRO_NAO_EXISTE_ARQUIVO, new Object[]{caminhoArquivo}));
        }
        if (!certFile.canRead()) {
            throw new HSCertificadoException(MessageFormat.format(CertificadoConstants.ERRO_ARQUIVO_PERMISSAO_LEITURA, new Object[]{caminhoArquivo}));
        }
        try {
            KeyStore keyStore;
            String alias;
            X509Certificate certificate;
            PrivateKey privateKey;
            Enumeration e;
            keyStore = KeyStore.getInstance(KEYSTORE_INSTANCE);
            keyStore.load(new FileInputStream(certFile), senha.toCharArray());
            alias = "";
            certificate = null;
            privateKey = null;
            for (e = keyStore.aliases(); e.hasMoreElements();) {
                alias = (String) e.nextElement();
                certificate = (X509Certificate) keyStore.getCertificate(alias);
                privateKey = (PrivateKey) keyStore.getKey(alias, senha.toCharArray());
                certificadosValidos.put(certificate, privateKey);
            }
        } catch (IOException e) {
            throw new HSCertificadoSenhaException(CertificadoConstants.ERRO_SENHA);
        } catch (Exception e) {
            throw new HSCertificadoException(CertificadoConstants.ERRO_PARSER_CERTIFICADO);
        }
        if (certificadosValidos.size() > 0) {
            return certificadosValidos;
        }

        return null;
    }

    /**
     * Valida o CNPJ com o do emitente.
     */
    private static boolean isCNPJEmitente(X509Certificate certificado, String cnpjBase) throws HSCertificadoInvalidoException {
        boolean retorno = false;
        if ((cnpjBase == null) || (cnpjBase.isEmpty()) || (CertificadoHelper.recuperarCNPJ(certificado).substring(0, 8).equals(cnpjBase.substring(0, 8)))) {
            retorno = true;
        }
        return retorno;
    }

    /**
     * Assina um XML.
     */
    public static String assinarXML(X509Certificate certificado, PrivateKey privateKey, String referenceURI, String conteudoXML, String cnpjBase) throws HSCertificadoException {
        String retorno = null;
        try {
            if (isCNPJEmitente(certificado, cnpjBase)) {
                String xml = conteudoXML;
                XMLSignatureFactory signatureFactory = XMLSignatureFactory.getInstance("DOM");
                List listTransforms = new ArrayList();
                listTransforms.add(signatureFactory.newTransform("http://www.w3.org/2000/09/xmldsig#enveloped-signature", (TransformParameterSpec) null));
                listTransforms.add(signatureFactory.newTransform("http://www.w3.org/TR/2001/REC-xml-c14n-20010315", (TransformParameterSpec) null));

                Reference reference = signatureFactory.newReference("#" + referenceURI, signatureFactory.newDigestMethod("http://www.w3.org/2000/09/xmldsig#sha1", null), listTransforms, null, null);
                SignedInfo signedInfo = signatureFactory.newSignedInfo(signatureFactory.newCanonicalizationMethod("http://www.w3.org/TR/2001/REC-xml-c14n-20010315", (C14NMethodParameterSpec) null), signatureFactory.newSignatureMethod("http://www.w3.org/2000/09/xmldsig#rsa-sha1", null), Collections.singletonList(reference));

                KeyInfoFactory keyInfoFactory = signatureFactory.getKeyInfoFactory();
                X509Data data = keyInfoFactory.newX509Data(Collections.singletonList(certificado));
                KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(data));

                DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
                documentBuilderFactory.setNamespaceAware(true);
                Document document = documentBuilderFactory.newDocumentBuilder().parse(new InputSource(new InputStreamReader(new ByteArrayInputStream(xml.getBytes()))));

                DOMSignContext signContext = new DOMSignContext(privateKey, document.getDocumentElement());
                XMLSignature signature = signatureFactory.newXMLSignature(signedInfo, keyInfo);
                signature.sign(signContext);

                TransformerFactory transformerFactory = TransformerFactory.newInstance();
                Transformer transformer = transformerFactory.newTransformer();
                StringWriter sw = new StringWriter();
                StreamResult streamResult = new StreamResult(sw);

                transformer.transform(new DOMSource(document), streamResult);
                retorno = sw.toString();
            } else {
                throw new HSCNPJInvalidoException(CertificadoConstants.ERRO_CNPJ_INVALIDO);
            }
        } catch (Exception e) {
            throw new HSCertificadoException(CertificadoConstants.ERRO_VALIDACAO_ASSINATURA);
        }

        return retorno;
    }

    /**
     * Valida a assinatura de um XML.
     */
    public static boolean validarAssinaturaXML(String conteudoXML) throws HSCertificadoException {
        int indexSignature = 0;

        boolean coreValidity = false;
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            Document doc = dbf.newDocumentBuilder().parse(new InputSource(new InputStreamReader(new ByteArrayInputStream(conteudoXML.getBytes()))));
            NodeList nl = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature");

            if (nl.getLength() == 0) {
                throw new HSCertificadoException(CertificadoConstants.ERRO_NAO_EXISTE_ASSINATURA);
            }

            XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
            DOMValidateContext valContext = null;
            XMLSignature signature = null;
            do {
                valContext = new DOMValidateContext(new KeyValueKeySelector(), nl.item(indexSignature++));

                signature = fac.unmarshalXMLSignature(valContext);

                coreValidity = signature.validate(valContext);
                if (!coreValidity) {
                    break;
                }
            } while (indexSignature < nl.getLength());
        } catch (Exception e) {
            throw new HSCertificadoException(CertificadoConstants.ERRO_VALIDACAO_ASSINATURA);
        }
        return coreValidity;
    }

    /**
     * Valida um certificado e retorna as mensagens de erro.
     */
    public static List<String> validarCertificado(X509Certificate certificado, String cnpj) {
        List<String> erros = new ArrayList<String>();
        try {
            CertificadoHelper.validarVersao(certificado);
        } catch (HSCertificadoInvalidoException e) {
            erros.add(e.getMessage());
        }
        try {
            CertificadoHelper.validarEntidadeFinal(certificado);
        } catch (HSCertificadoInvalidoException e) {
            erros.add(e.getMessage());
        }
        try {
            CertificadoHelper.validarPeriodo(certificado);
        } catch (HSCertificadoInvalidoException e) {
            erros.add(e.getMessage());
        }
        try {
            CertificadoHelper.validarOtherNameCNPJ(certificado);
        } catch (HSCertificadoInvalidoException e) {
            erros.add(e.getMessage());
        }
        try {
            if (!isCNPJEmitente(certificado, cnpj)) {
                erros.add(CertificadoConstants.ERRO_CNPJ_INVALIDO);
            }
        } catch (HSCertificadoInvalidoException e) {
            erros.add(CertificadoConstants.ERRO_CNPJ_INVALIDO);
        }
        try {
            CertificadoHelper.validarNaoRepudio(certificado);
        } catch (HSCertificadoInvalidoException e) {
            erros.add(e.getMessage());
        }
        try {
            CertificadoHelper.validarCertificadoAssinatura(certificado);
        } catch (HSCertificadoInvalidoException e) {
            erros.add(e.getMessage());
        }
        try {
            CertificadoHelper.validarCertificadoAutenticacao(certificado);
        } catch (HSCertificadoInvalidoException e) {
            erros.add(e.getMessage());
        }
        return erros;
    }
}
